/*
@author harley-dog

1.计算压力支撑位
2.计算布林轨
3.确定有效压力支撑位
    确定方法
    1.从昨日起往前循环找压力位，如果上一个压力位低于当前压力位，则停止查找，认为该压力位为有效压力位
    2.从昨日起往前循环找支撑位，如果上一个支撑位高于当前支撑位，则停止查找，认为该支撑位为有效支撑位
4.确定布林轨有效值
    1.布林轨最高值确定方法
    从有效压力位截止到昨天为止，找出这中间最高的一个布林轨中值，确定为布林轨最高值
    2.布林轨最低值确定方法
    从有效支撑位截止到昨天为止，找出这中间最低的一个布林轨中值，确定为布林轨最低值
5.判定趋势方法
    1.昨天无趋势的判定方法（首次判定）
        多头趋势：当天布林轨中值 高于 布林轨最高值 和 布林轨最低值
        空头趋势：当天布林轨中值 低于 布林轨最低值 和 布林轨最高值
        震荡趋势：以上两者都不满足，则为震荡趋势

    2.昨天有趋势的判定方法（趋势逆转）
        多头趋势 -> 震荡趋势 ：昨天为多头趋势，今天布林轨 低于 昨天的布林轨中值 ，则转为震荡趋势
        多头趋势 -> 空头趋势 ：昨天为多头趋势，今天布林轨 低于 布林轨最低值 ，且在有效支撑位到昨天之间，存在收盘价低于有效支撑位价格的一根阴柱

        空头趋势 -> 震荡趋势 ：昨天为空头趋势，今天布林轨 高于 昨天的布林轨中值 ，则转为震荡趋势
        空头趋势 -> 多头趋势 ：昨天为空头趋势，今天布林轨 高于 布林轨最低值 ，且在有效压力位到昨天之间，存在收盘价高于有效压力位价格的一根阳柱

        震荡趋势 -> 多头趋势 ：昨天为震荡趋势，今天布林轨 高于 布林轨最高值 ，且在有效压力位到昨天之间，存在收盘价高于有效压力位价格的一根阳柱
        震荡趋势 -> 空头趋势 ：昨天为震荡趋势，今天布林轨 低于 布林轨最低值 ，且在有效支撑位到昨天之间，存在收盘价低于有效支撑位价格的一根阴柱
*/
package cn.skyquant.quant4j.jforex.strategy.trend;

import cn.skyquant.quant4j.api.enums.Trend;
import cn.skyquant.quant4j.api.quant.TimeBar;
import cn.skyquant.quant4j.api.ylzc.YlzcDTO;
import cn.skyquant.quant4j.api.ylzc.ZigZag;
import cn.skyquant.quant4j.jforex.sdk.strategy.InstrumentEntity;
import cn.skyquant.quant4j.jforex.sdk.strategy.MA;
import cn.skyquant.quant4j.jforex.sdk.strategy.TimeBoll;
import cn.skyquant.quant4j.jforex.sdk.util.QuantUtil;
import cn.skyquant.quant4j.sdk.enums.YlzcType;
import cn.skyquant.quant4j.sdk.util.time.CalendarUtils;
import com.dukascopy.api.*;
import com.dukascopy.api.feed.IBarFeedListener;
import com.dukascopy.api.feed.IFeedDescriptor;

import java.util.*;

@Library("quant4j-api.jar;quant4j-sdk.jar;quant4j-jforex-sdk.jar;")
public class TrendTest implements IStrategy, IBarFeedListener {
    protected IEngine engine;
    protected IConsole console;
    protected IHistory history;
    protected IContext context;
    protected IIndicators indicators;
    protected IAccount account;

    @Configurable("上一次处于震荡期的时间")
    public Date lastTime;
    @Configurable("货币")
    public Instrument i1 = Instrument.EURUSD;
    @Configurable("大K线")
    public Period bigPeriod = Period.DAILY;
    @Configurable("小周期均线:计算K柱个数(也用作ATR个数)")
    public int small_nums = 30;
    @Configurable("长周期均线:计算K柱个数(也用作ATR个数)")
    public int big_nums = 74;
    @Configurable("小周期均线:价格选择")
    public IIndicators.AppliedPrice appliedPrice = IIndicators.AppliedPrice.WEIGHTED_CLOSE;
    @Configurable("小周期均线:均线类型")
    public IIndicators.MaType boll_ma_type = IIndicators.MaType.WMA;
    @Configurable("布林轨:宽度")
    public double boll_width = 2;
    @Configurable("压力支撑位:计算天数")
    public int zigzag_days = 100;
    @Configurable("压力支撑位:extDepth")
    public int zigzag_extDepth = 12;
    @Configurable("压力支撑位:ExtDeviation")
    public int zigzag_ExtDeviation = 6;
    @Configurable("压力支撑位:extDepth")
    public int zigzag_Backstep = 3;
    @Configurable("趋势日志")
    public boolean log_trend = true;
    @Configurable("调试日志")
    public boolean log_debug = false;

    final Map<Instrument, InstrumentEntity> insMap = new HashMap<>();//交易品种实体
    final OfferSide offerSide = OfferSide.BID;
    final Filter filter = Filter.WEEKENDS;
    final LinkedList<DateTrend> bigTrendList = new LinkedList<DateTrend>();
    final LinkedList<DateTrend> smallTrendList = new LinkedList<DateTrend>();

    @Override
    public void onStart(IContext context) throws JFException {
        this.engine = context.getEngine();
        this.console = context.getConsole();
        this.history = context.getHistory();
        this.context = context;
        this.indicators = context.getIndicators();
        this.account = context.getAccount();
        InstrumentEntity ie = new InstrumentEntity(i1);
        insMap.put(i1, ie);
        IChart chart = context.getChart(i1);
        if (lastTime == null) {
            console.getErr().println("start time can't be null");
            context.stop();
            return;
        }
        if (chart != null) {
            IFeedDescriptor feedDescriptor = chart.getFeedDescriptor();
            if (feedDescriptor != null) {
                feedDescriptor.setPeriod(bigPeriod);
                feedDescriptor.setOfferSide(offerSide);
                chart.setFeedDescriptor(feedDescriptor);
                chart.add(indicators.getIndicator("MA"), new Object[]{small_nums, boll_ma_type.ordinal()});
                chart.add(indicators.getIndicator("BBANDS"), new Object[]{big_nums, boll_width, boll_width, boll_ma_type.ordinal()});
                chart.add(indicators.getIndicator("ZigZag"), new Object[]{zigzag_extDepth, zigzag_ExtDeviation, zigzag_Backstep});
                chart.repaint();
            }
        }
        context.subscribeToBarsFeed(i1, bigPeriod, offerSide, this);

        //这个策略的先决条件，就是先确定指定的那一天为震荡市，然后开始往后一天天推
        long beginTime = history.getBarStart(bigPeriod, lastTime.getTime());
        DateTrend trend = new DateTrend(beginTime, Trend.SHOCK);
        console.getOut().println(trend.toString());
        bigTrendList.addLast(trend);
        smallTrendList.addLast(trend);
        while (true) {
            DateTrend dateTrend = bigTrendList.getLast();
            long nextStart = history.getNextBarStart(bigPeriod, dateTrend.time);
            if (nextStart >= context.getTime()) {
                break;
            }
            try {
                calc(i1, bigPeriod, offerSide, nextStart,bigTrendList,big_nums);
                calc(i1, bigPeriod, offerSide, nextStart,smallTrendList,small_nums);
                if (log_trend) {
                    console.getOut().println(
                            (bigPeriod.getInterval()>=Period.DAILY.getInterval()?CalendarUtils.formatGMTSimple(nextStart):CalendarUtils.formatGMTStandard(nextStart))+
                                    ",big:["+bigTrendList.getLast().trend.name()+"],small:["+smallTrendList.getLast().trend.name()+"]");
                }
            } catch (JFException e) {
                console.getErr().println("error:" + e.getMessage());
            }
        }
        return;
    }


    @Override
    public void onBar(Instrument instrument, Period period, OfferSide offerSide, IBar bar) {
        try {
            long time = bar.getTime();
            calc(instrument, period, offerSide, bar.getTime(),bigTrendList,big_nums);
            calc(instrument, period, offerSide, bar.getTime(),smallTrendList,small_nums);
            if (log_trend) {
                console.getOut().println(
                        (period.getInterval()>=Period.DAILY.getInterval()?CalendarUtils.formatGMTSimple(time):CalendarUtils.formatGMTStandard(time))+
                                ",big:["+bigTrendList.getLast().trend.name()+"],small:["+smallTrendList.getLast().trend.name()+"]");
            }
        } catch (JFException e) {
            console.getErr().println("error:" + e.getMessage());
        }
    }

    public void calc(Instrument instrument, Period period, OfferSide offerSide, long time,LinkedList<DateTrend> trendList,int nums) throws JFException {
        InstrumentEntity ie = insMap.get(instrument);
        DateTrend lastTrend = trendList.getLast();//根据昨天的趋势来判断今天的趋势
        double[] zigzags = indicators.zigzag(instrument, period, offerSide, zigzag_extDepth, zigzag_ExtDeviation, zigzag_Backstep, filter, zigzag_days, time, 0);
        double[] mas = indicators.ma(instrument, period, offerSide, appliedPrice, nums, boll_ma_type, filter, zigzag_days, time, 0);
        List<IBar> iBarList = history.getBars(instrument, period, offerSide, filter, zigzag_days, time, 0);
        if (zigzags.length != zigzag_days || mas.length != zigzag_days || iBarList == null || iBarList.size() != zigzag_days) {
            throw new RuntimeException("calc [" + zigzag_days + "] error");
        }
        List<TimeBar> barList = QuantUtil.batchConvertIBarToTimeBar(iBarList);
        List<MA> maList = QuantUtil.batchConvertDoubleToMA(mas, barList);
        List<ZigZag> ylzcList = QuantUtil.batchConvertZigZagToYlzc(zigzags, barList);
        //确定有效压力位和有效支撑位
        if (ylzcList.size() < 3) {
            return;
        }
        MA ma_1 = maList.get(maList.size() - 1);
        MA ma_2 = maList.get(maList.size() - 2);
        MA ma_3 = maList.get(maList.size() - 3);
        ZigZag maxYlw = findMaxYlw(ylzcList, 1);
        ZigZag minZcw = findMinZcw(ylzcList, 1);
        //确定布林轨最高值和最低值
        //1.布林轨最高点是从有效压力位后找出最高的布林轨数据
        MA maxHighMA = maList.stream().filter(x -> x.time >= maxYlw.time && x.time < time).max(Comparator.comparingDouble(MA::getValue)).get();
        MA minLowMA = maList.stream().filter(x -> x.time >= minZcw.time && x.time < time).min(Comparator.comparingDouble(MA::getValue)).get();
        if (log_debug) {
            console.getOut().println("----------");
            console.getOut().println("maxYlw=" + maxYlw);
            console.getOut().println("minZcw=" + minZcw);
            console.getOut().println("maxHighBoll=" + CalendarUtils.formatGMTSimple(maxHighMA.time) + ":" + ie.formatPrice(maxHighMA.value));
            console.getOut().println("minLowBoll=" + CalendarUtils.formatGMTSimple(minLowMA.time) + ":" + ie.formatPrice(minLowMA.value));
            console.getOut().println("m=" + ma_1.value);
        }
        DateTrend trend = null;
        switch (lastTrend.trend) {
            case SHOCK:
                //如果是震荡市
                if (ma_1.value > maxHighMA.value) {
                    long count = barList.stream().filter(x -> x.time > maxYlw.time).filter(x -> x.close > maxYlw.value).count();
                    if (count > 0) {
                        trend = new DateTrend(time, Trend.BULL);
                    }
                }
                if (ma_1.value < minLowMA.value) {
                    long count = barList.stream().filter(x -> x.time > minZcw.time).filter(x -> x.close > minZcw.value).count();
                    if (count > 0) {
                        trend = new DateTrend(time, Trend.BEAR);
                    }
                }
                break;
            case BULL:
            case V:
                //如果是牛市或大V
                if(ma_2.value - ma_1.value > ma_2.value - ma_3.value){
                    trend = new DateTrend(time,Trend.SHOCK);
                }
                break;
            case BEAR:
            case RV:
                //如果是熊市或大RV
                if(ma_1.value - ma_2.value > ma_3.value - ma_2.value){
                    trend = new DateTrend(time,Trend.SHOCK);
                }
                break;
        }
        if (trend == null) {
            trend = new DateTrend(time, lastTrend.trend);
        }
        trendList.addLast(trend);
        if (log_debug) {
            console.getOut().println("----------");
        }
    }

    private ZigZag findMaxYlw(List<ZigZag> ylzcList, int k) {
        ZigZag maxY = null;
        for (int i = ylzcList.size() - k - 1; i >= 0; i--) {
            ZigZag ylzc = ylzcList.get(i);
            if (ylzc.type != YlzcType.Y) {
                continue;
            }
            if (maxY == null) {
                maxY = ylzc;
                continue;
            }
            if (ylzc.value > maxY.value) {
                maxY = ylzc;
                continue;
            }
            if (maxY != null && ylzc.value < maxY.value) {
                break;
            }
        }
        return maxY;
    }

    private ZigZag findMinZcw(List<ZigZag> ylzcList, int k) {
        ZigZag minZ = null;
        for (int i = ylzcList.size() - k - 1; i >= 0; i--) {
            ZigZag ylzc = ylzcList.get(i);
            if (ylzc.type != YlzcType.Z) {
                continue;
            }
            if (minZ == null) {
                minZ = ylzc;
                continue;
            }
            if (ylzc.value < minZ.value) {
                minZ = ylzc;
                continue;
            }
            if (minZ != null && ylzc.value > minZ.value) {
                break;
            }
        }
        return minZ;
    }

    private YlzcDTO findYlzcLimit(YlzcDTO ylzc, int k, List<YlzcDTO> ylzcList) {
        if (ylzc.type == YlzcType.Y) {
            YlzcDTO maxY = ylzc;
            for (int i = ylzcList.size() - k - 1; i >= 0; i--) {
                YlzcDTO ylzcDTO = ylzcList.get(i);
                if (ylzcDTO.type == YlzcType.Y && ylzcDTO.value > maxY.value) {
                    maxY = ylzc;
                } else {
                    return maxY;
                }
            }
        } else if (ylzc.type == YlzcType.Z) {
            YlzcDTO minZ = ylzc;
            for (int i = ylzcList.size() - k - 1; i >= 0; i--) {
                YlzcDTO ylzcDTO = ylzcList.get(i);
                if (ylzcDTO.type == YlzcType.Z && ylzcDTO.value < minZ.value) {
                    minZ = ylzc;
                } else {
                    return minZ;
                }
            }
        }
        return null;
    }

    @Override
    public void onTick(Instrument instrument, ITick tick) throws JFException {
    }

    @Override
    public void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException {
    }

    @Override
    public void onMessage(IMessage message) throws JFException {
    }

    @Override
    public void onAccount(IAccount account) throws JFException {
    }

    @Override
    public void onStop() throws JFException {
    }

    class DateTrend {
        final long time;
        final Trend trend;

        public DateTrend(long time, Trend trend) {
            this.time = time;
            this.trend = trend;
        }

        @Override
        public String toString() {
            return CalendarUtils.formatGMTStandard(time) + ":" + trend.name();
        }
    }

    class DateCalcTrend extends DateTrend {
        final TimeBoll maxHigh;
        final TimeBoll minLow;
        final ZigZag ylw;
        final ZigZag zcw;

        public DateCalcTrend(long time, Trend trend, TimeBoll maxHigh, TimeBoll minLow, ZigZag ylw, ZigZag zcw) {
            super(time, trend);
            this.maxHigh = maxHigh;
            this.minLow = minLow;
            this.ylw = ylw;
            this.zcw = zcw;
        }

        public String toString() {
            return CalendarUtils.formatGMTStandard(time) +
                    "," + trend.name() +
                    ",ylw=" + ylw +
                    ",zcw=" + zcw +
                    ",boll.m.maxHigh=" + CalendarUtils.formatGMTSimple(maxHigh.time) + "," + maxHigh.m +
                    ",boll.m.minLow=" + CalendarUtils.formatGMTSimple(minLow.time) + "," + minLow.m;
        }
    }

}
